using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Windows.Forms;
using System.Xml;
using JetBrains.ReSharper.TaskRunnerFramework;
using log4net;

namespace JamesKovacs.ReSharper.UnitTestSupport.VstsUnit {
    internal class VstsUnitTaskRunner : RemoteTaskRunner {
        private static readonly ILog s_Logger = LogManager.GetLogger(typeof(VstsUnitTaskRunner));
        private const string resultsFileName = "__VstsUnitTestResultsFile__.trx";
        private const string xpathTestResult = "/Tests/UnitTestResult[testName/text() = '{0}']";
        private const string xpathOutcome = "outcome/value__";
        private const string xpathStdout = "stdout";
        private const string xpathErrorInfoMessage = "errorInfo/message";
        private const string xpathErrorInfoStackTrace = "errorInfo/stackTrace";
        private const string xpathStderr = "stderr";
        private const string xpathDebugTrace = "debugTrace";
        private const string xpathDuration = "duration";
        private string m_UnitTestAssemblyFullPath;
        private string m_WorkingDirectory;
        private string m_ResultsFileFullPath;

        public VstsUnitTaskRunner(IRemoteTaskServer server) : base(server) {
            MessageBox.Show("Creating VstsUnitTaskRunner");
            server.Connect();
            server.TaskOutput(null, "output test", TaskOutputType.DEBUGTRACE);
            server.TaskOutput(null, "output test", TaskOutputType.STDERR);
            server.TaskOutput(null, "output test", TaskOutputType.STDOUT);
            Debugger.Break();
            VstsUnitSetup.Initialize();
        }

        public override void ConfigureAppDomain(TaskAppDomainConfiguration configuration) {
            // NOP
        }

        public override TaskResult Start(TaskExecutionNode node) {
            s_Logger.DebugFormat("Starting {0}", node.RemoteTask.RunnerID);
            Server.TaskOutput(node.RemoteTask, "output test - start", TaskOutputType.DEBUGTRACE);
            Server.TaskOutput(node.RemoteTask, "output test - start", TaskOutputType.STDERR);
            Server.TaskOutput(node.RemoteTask, "output test - start", TaskOutputType.STDOUT);
            PopulateMemberVariables(node);
            CleanupRun(m_WorkingDirectory, m_ResultsFileFullPath);
            return TaskResult.Success;
        }

        public override TaskResult Execute(TaskExecutionNode node) {
            s_Logger.DebugFormat("Executing {0}", node.RemoteTask.RunnerID);
            VstsUnitTestTaskBase testTask = PopulateMemberVariables(node);

            // TODO: Fix this!!!
            string fixtureName = "foo";
            string[] methodNames = testTask.MethodNames;
            try {
                LogTestMethodNames(fixtureName, methodNames);
                RunMSTest(m_UnitTestAssemblyFullPath, methodNames, m_WorkingDirectory);
                ParseAndDisplayResultsFile(testTask, methodNames);
                return TaskResult.Success;
            } catch(Exception ex) {
                s_Logger.Error("Error running unit tests", ex);
                MessageBox.Show(ex.ToString(), "Error Running Unit Tests");
                return TaskResult.Exception;
            }
        }

        public override TaskResult Finish(TaskExecutionNode node) {
            s_Logger.DebugFormat("Finishing {0}", node.RemoteTask.RunnerID);
            PopulateMemberVariables(node);
            CleanupRun(m_WorkingDirectory, m_ResultsFileFullPath);
            return TaskResult.Success;
        }

        private VstsUnitTestTaskBase PopulateMemberVariables(TaskExecutionNode node) {
            VstsUnitTestTaskBase testTask = node.RemoteTask as VstsUnitTestTaskBase;
            if(testTask == null) {
                throw new ArgumentException("Node is not part of VstsUnit", "node");
            }
            m_UnitTestAssemblyFullPath = testTask.AssemblyLocation;
            m_WorkingDirectory = Path.GetDirectoryName(m_UnitTestAssemblyFullPath);
            m_ResultsFileFullPath = Path.Combine(m_WorkingDirectory, resultsFileName);
            return testTask;
        }

        private static void LogTestMethodNames(string fixtureName, string[] methodNames) {
            if(s_Logger.IsDebugEnabled) {
                StringBuilder sb = new StringBuilder();
                sb.AppendFormat("Fixture: {0}{1}", fixtureName, Environment.NewLine);
                for(int i = 0; i < methodNames.Length; i++) {
                    sb.AppendFormat("{0}{1}", methodNames[i], Environment.NewLine);
                }
                s_Logger.Debug(sb.ToString());
            }
        }

        private static void CleanupRun(string workingDirectory, string resultsFileFullPath) {
            DirectoryInfo workingDirectoryInfo = new DirectoryInfo(workingDirectory);
            bool inFound = false;
            bool outFound = false;
            foreach(DirectoryInfo dirInfo in workingDirectoryInfo.GetDirectories()) {
                foreach(DirectoryInfo subdirInfo in dirInfo.GetDirectories()) {
                    switch(subdirInfo.Name) {
                        case "In":
                            inFound = true;
                            break;
                        case "Out":
                            outFound = true;
                            break;
                    }
                    if(inFound && outFound) {
                        dirInfo.Delete(true);
                    }
                }
            }

            if(File.Exists(resultsFileFullPath)) {
                try {
                    File.Delete(resultsFileFullPath);
                } catch(IOException ex) {
                    throw new IOException(string.Format("Unable to delete temporary results file, {0}, because it is currently in use.", resultsFileFullPath), ex);
                }
            }
        }

        private static void RunMSTest(string unitTestAssemblyFullPath, string[] methodNames, string workingDirectory) {
            if(methodNames == null || methodNames.Length == 0) {
                throw new ArgumentNullException("methodNames");
            }
            using(Process msTestProcess = new Process()) {
                ProcessStartInfo processStartInfo = new ProcessStartInfo(GetMSTestPath(), BuildMSTestParameters(unitTestAssemblyFullPath, methodNames));
                processStartInfo.WorkingDirectory = workingDirectory;
                processStartInfo.UseShellExecute = false;
                processStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
                processStartInfo.CreateNoWindow = true;
                msTestProcess.StartInfo = processStartInfo;
                msTestProcess.Start();
                msTestProcess.WaitForExit();
            }
        }

        private void ParseAndDisplayResultsFile(RemoteTask task, string[] methodNames) {
            TaskResult[] testResults = new TaskResult[methodNames.Length];
            XmlDocument xmlResults = new XmlDocument();
            // The file might not exist if all tests to be run were marked with Ignore.
            // If this happens, just follow normal code path with empty XmlDocument.
            if(File.Exists(m_ResultsFileFullPath)) {
                xmlResults.Load(m_ResultsFileFullPath);
            }
            for(int i = 0; i < methodNames.Length; i++) {
                string methodName = methodNames[i];
                XmlNode testResultNode = xmlResults.SelectSingleNode(string.Format(xpathTestResult, methodName));
                TimeSpan duration;
                // MSTest ignored the test likely due to the Ignore attribute
                if(testResultNode == null) {
                    testResults[i] = TaskResult.Skipped;
                    duration = new TimeSpan(0);
                } else {
                    NotifyConsole(task, testResultNode.SelectSingleNode(xpathStdout), TaskOutputType.STDOUT);
                    NotifyConsole(task, testResultNode.SelectSingleNode(xpathErrorInfoMessage), TaskOutputType.STDERR);
                    NotifyConsole(task, testResultNode.SelectSingleNode(xpathErrorInfoStackTrace), TaskOutputType.STDERR);
                    NotifyConsole(task, testResultNode.SelectSingleNode(xpathStderr), TaskOutputType.STDERR);
                    NotifyConsole(task, testResultNode.SelectSingleNode(xpathDebugTrace), TaskOutputType.DEBUGTRACE);
                    duration = TimeSpan.Parse(testResultNode.SelectSingleNode(xpathDuration).InnerXml);
                    TestOutcome outcome = (TestOutcome)Enum.Parse(typeof(TestOutcome), testResultNode.SelectSingleNode(xpathOutcome).InnerXml);
                    testResults[i] = ConvertToJetbrainsTestResult(outcome);
                }
                NotifyTestFinished(methodName, testResults[i], string.Empty, duration.TotalMilliseconds);
            }
            NotifyFixtureTeardown();
            TaskResult fixtureResult = CombineTaskResults(testResults);
            NotifyFixtureFinished(fixtureResult);
        }

        private void NotifyConsole(RemoteTask task, XmlNode xmlNode, TaskOutputType testOutputType) {
            if(xmlNode == null) {
                return;
            }
            string output = xmlNode.InnerXml;
            if(!string.IsNullOrEmpty(output)) {
                if(!output.EndsWith(Environment.NewLine)) {
                    output += Environment.NewLine;
                }
                this.Server.TaskOutput(task, output, testOutputType);
            }
        }

        private void NotifyTestFinished(string methodName, TaskResult testResult, string message, double time) {
            //            Listener.OnTestFinished(m_RunnerId, m_UnitTestAssemblyName, fixtureName, methodName, testResult, message, time);
        }

        private static TaskResult CombineTaskResults(TaskResult[] testResults) {
            if(Array.FindAll(testResults, delegate(TaskResult result) { return result == TaskResult.Exception; }).Length > 0) {
                return TaskResult.Exception;
            }
            if(Array.FindAll(testResults, delegate(TaskResult result) { return result == TaskResult.Error; }).Length > 0) {
                return TaskResult.Error;
            }
            if(Array.FindAll(testResults, delegate(TaskResult result) { return result == TaskResult.Skipped; }).Length > 0) {
                return TaskResult.Skipped;
            }
            return TaskResult.Success;
        }

        private static TaskResult ConvertToJetbrainsTestResult(TestOutcome outcome) {
            TaskResult jetbrainsResult;
            switch(outcome) {
                case TestOutcome.Passed:
                case TestOutcome.PassedButRunAborted:
                    jetbrainsResult = TaskResult.Success;
                    break;
                case TestOutcome.Failed:
                case TestOutcome.Timeout:
                case TestOutcome.Aborted:
                    jetbrainsResult = TaskResult.Error;
                    break;
                case TestOutcome.Error:
                    jetbrainsResult = TaskResult.Exception;
                    break;
                case TestOutcome.NotExecuted:
                    jetbrainsResult = TaskResult.Skipped;
                    break;
                case TestOutcome.Completed:
                case TestOutcome.Disconnected:
                case TestOutcome.InProgress:
                case TestOutcome.Inconclusive:
                case TestOutcome.NotRunnable:
                case TestOutcome.Pending:
                case TestOutcome.Warning:
                default:
                    throw new ApplicationException(string.Format("Unexpected TestOutcome: {0}", outcome));
            }
            return jetbrainsResult;
        }

        private void NotifyFixtureTeardown() {
//            Listener.OnFixtureTeardown(m_RunnerId, m_UnitTestAssemblyName, fixtureName);
        }

        private void NotifyFixtureFinished(TaskResult fixtureResult) {
//            Listener.OnFixtureFinished(m_RunnerId, m_UnitTestAssemblyName, fixtureName, fixtureResult, string.Empty);
        }

        private static string GetMSTestPath() {
            string vsTools = Environment.GetEnvironmentVariable("VS80COMNTOOLS");
            if(string.IsNullOrEmpty(vsTools)) {
                throw new ApplicationException("Cannot locate MSTest.exe. VS80COMNTOOLS environment variable is not set.");
            }
            string msTestPath = Path.GetFullPath(Path.Combine(vsTools, @"..\IDE\MSTest.exe"));
            if(!File.Exists(msTestPath)) {
                throw new ApplicationException(string.Format("Cannot locate MSTest.exe. Expected location is {0}", msTestPath));
            }
            s_Logger.Debug(msTestPath);
            return msTestPath;
        }

        private static string BuildMSTestParameters(string unitTestAssemblyFullPath, string[] testNames) {
            // mstest /nologo /testcontainer:bin\debug\JamesKovacs.ExamplesContractManagerTests.dll /resultsfile:bin\debug\results.trx /test:AddMoney /test:AllocateMoney
            // N.B. Do not use /unique as mstest.exe will then skip some tests with the same root name. 
            //      e.g. Two tests: ShouldCalculatePayments and ShouldCalculatePaymentsWithInterest. 
            //      If /unique is specified, only ShouldCalculatePaymentsWithInterest will be called.
            // N.B. Do not use /noisolation to speed up tests as configuration files (app.config) will not be loaded.
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("/nologo /testcontainer:{0} /resultsfile:{1}", unitTestAssemblyFullPath, resultsFileName);
            foreach(string testName in testNames) {
                sb.AppendFormat(" /test:{0}", testName);
            }
            string msTestParameters = sb.ToString();
            s_Logger.Debug(msTestParameters);
            return msTestParameters;
        }
    }
}